Creating a simple action game

Written by Adok

In this article I will provide commented source code of my game "Evolution", which was made for the Ludum Dare 24 contest. It is a mixed 2D shooter/platform game and so you will see how games of these two genres are implemented. I use OpenGL for the graphics and the code contains some Windows-specific elements, so it will not be runnable under alternative platforms such as Linux or MacOS. But of course the general ideas are the same independently of the platform.

The Base Code

First of all I use some base code in my games that was partly taken from NeHe, the website with the famous OpenGL tutorial. Moreover, I make use of BASS, the sound library. So to compile the code without changes, you will have to download some version of BASS (I used 2.4) and include bass.h and bass.lib in your project. Alternatively, you can just remove the sound-specific code from game.cpp and game.h, but then of course there will be silence.

One of the pieces of code I took from NeHe is the window handling code, which consists of two files, NeHe_Window.cpp and NeHe_Window.h. Here is NeHe_Window.cpp:

#include "NeHe_Window.h"

void TerminateApplication (GL_Window *window)
{
	PostMessage (window->hWnd, WM_QUIT, 0, 0);
	g_isProgramLooping = FALSE;
}

void ToggleFullscreen (GL_Window *window)
{
	PostMessage (window->hWnd, WM_TOGGLEFULLSCREEN, 0, 0);
}

void ReshapeGL (int width, int height)
{
	glViewport (0, 0, (GLsizei)(width), (GLsizei)(height));
	glMatrixMode (GL_PROJECTION);
	glLoadIdentity ();
	gluPerspective (45.0f, (GLfloat)(width) / (GLfloat)(height), 0.1f, 100.0f);		
	glMatrixMode (GL_MODELVIEW);
	glLoadIdentity ();
}

BOOL ChangeScreenResolution (int width, int height, int bitsPerPixel)
{
	DEVMODE dmScreenSettings;
	ZeroMemory (&dmScreenSettings, sizeof (DEVMODE));
	dmScreenSettings.dmSize			= sizeof (DEVMODE);
	dmScreenSettings.dmPelsWidth		= width;
	dmScreenSettings.dmPelsHeight		= height;
	dmScreenSettings.dmBitsPerPel		= bitsPerPixel;
	dmScreenSettings.dmFields		= DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT;
	if (ChangeDisplaySettings (&dmScreenSettings, CDS_FULLSCREEN) != DISP_CHANGE_SUCCESSFUL)
	{
		return FALSE;
	}
	return TRUE;
}

BOOL CreateWindowGL (GL_Window *window)
{
	__int64	timer;
	DWORD windowStyle = WS_OVERLAPPEDWINDOW;
	DWORD windowExtendedStyle = WS_EX_APPWINDOW;

	PIXELFORMATDESCRIPTOR pfd =
	{
		sizeof (PIXELFORMATDESCRIPTOR),
		1,
		PFD_DRAW_TO_WINDOW |
		PFD_SUPPORT_OPENGL |
		PFD_DOUBLEBUFFER,
		PFD_TYPE_RGBA,
		window->init.bitsPerPixel,
		0, 0, 0, 0, 0, 0,
		0,
		0,
		0,
		0, 0, 0, 0,
		16,
		0,
		0,
		PFD_MAIN_PLANE,
		0,
		0, 0, 0
	};

	RECT windowRect = {0, 0, window->init.width, window->init.height};

	GLuint PixelFormat;

	if (window->init.isFullScreen == TRUE)
	{
		if (ChangeScreenResolution (window->init.width, 
		window->init.height, window->init.bitsPerPixel) == FALSE)
		{
			MessageBox (HWND_DESKTOP, "Mode Switch Failed.\nRunning In Windowed Mode.",
			"Error", MB_OK | MB_ICONEXCLAMATION);
			window->init.isFullScreen = FALSE;
		}
		else
		{
			ShowCursor (FALSE);
			windowStyle = WS_POPUP;
			windowExtendedStyle |= WS_EX_TOPMOST;
		}
	}
	else
		AdjustWindowRectEx (&windowRect, windowStyle, 0, windowExtendedStyle);

	window->hWnd = CreateWindowEx (windowExtendedStyle,
			window->init.application->className,
			window->init.title,
			windowStyle,
			0, 0,
			windowRect.right - windowRect.left,
			windowRect.bottom - windowRect.top,
			HWND_DESKTOP,
			0,
			window->init.application->hInstance,
			window);

	if (window->hWnd == 0)
	{
		return FALSE;
	}

	window->hDC = GetDC (window->hWnd);
	if (window->hDC == 0)
	{
		DestroyWindow (window->hWnd);
		window->hWnd = 0;
		return FALSE;
	}

	PixelFormat = ChoosePixelFormat (window->hDC, &pfd);
	if (PixelFormat == 0)
	{
		ReleaseDC (window->hWnd, window->hDC);
		window->hDC = 0;
		DestroyWindow (window->hWnd);
		window->hWnd = 0;
		return FALSE;
	}

	if (SetPixelFormat (window->hDC, PixelFormat, &pfd) == FALSE)
	{
		ReleaseDC (window->hWnd, window->hDC);
		window->hDC = 0;
		DestroyWindow (window->hWnd);
		window->hWnd = 0;
		return FALSE;
	}

	window->hRC = wglCreateContext (window->hDC);
	if (window->hRC == 0)
	{
		ReleaseDC (window->hWnd, window->hDC);
		window->hDC = 0;
		DestroyWindow (window->hWnd);
		window->hWnd = 0;
		return FALSE;
	}

	if (wglMakeCurrent (window->hDC, window->hRC) == FALSE)
	{
		wglDeleteContext (window->hRC);
		window->hRC = 0;
		ReleaseDC (window->hWnd, window->hDC);
		window->hDC = 0;
		DestroyWindow (window->hWnd);
		window->hWnd = 0;
		return FALSE;
	}

	ShowWindow (window->hWnd, SW_NORMAL);
	window->isVisible = TRUE;

	ReshapeGL (window->init.width, window->init.height);

	ZeroMemory (window->keys, sizeof (Keys));

	window->lastTickCount = GetTickCount ();

	return TRUE;
}

BOOL DestroyWindowGL (GL_Window* window)
{
	if (window->hWnd != 0)
	{	
		if (window->hDC != 0)
		{
			wglMakeCurrent (window->hDC, 0);
			if (window->hRC != 0)
			{
				wglDeleteContext (window->hRC);
				window->hRC = 0;
			}
			ReleaseDC (window->hWnd, window->hDC);
			window->hDC = 0;
		}
		DestroyWindow (window->hWnd);
		window->hWnd = 0;
	}

	if (window->init.isFullScreen)
	{
		ChangeDisplaySettings (NULL, 0);
		ShowCursor (TRUE);
	}	
	return TRUE;
}

BOOL RegisterWindowClass (Application* application, void *WindowProc)
{
	WNDCLASSEX windowClass;
	ZeroMemory (&windowClass, sizeof (WNDCLASSEX));
	windowClass.cbSize			= sizeof (WNDCLASSEX);
	windowClass.style			= CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
	windowClass.lpfnWndProc		= (WNDPROC)(WindowProc);
	windowClass.hInstance		= application->hInstance;
	windowClass.hbrBackground	= (HBRUSH)COLOR_WINDOW+1;
	windowClass.hCursor			= LoadCursor(NULL, IDC_ARROW);
	windowClass.lpszClassName	= application->className;
	if (RegisterClassEx (&windowClass) == 0)
	{
		MessageBox (HWND_DESKTOP, "RegisterClassEx Failed!", "Error", MB_OK | MB_ICONEXCLAMATION);
		return FALSE;
	}
	return TRUE;
}

And here's the other file, NeHe_Window.h:

#ifndef _NEHE_WINDOW_H_
#define _NEHE_WINDOW_H_

#include <windows.h>
#include <GL/gl.h>
#include <GL/glu.h>

#include "NeHeGL.h"

class GL_Window
{
public:
	Keys				*keys;
	HWND				hWnd;
	HDC					hDC;
	HGLRC				hRC;
	GL_WindowInit		init;
	BOOL				isVisible;
	DWORD				lastTickCount;
};

void TerminateApplication (GL_Window *window);
void ToggleFullscreen (GL_Window *window);
void ReshapeGL (int width, int height);
BOOL ChangeScreenResolution (int width, int height, int bitsPerPixel);
BOOL CreateWindowGL (GL_Window *window);
BOOL DestroyWindowGL (GL_Window *window);
BOOL RegisterWindowClass (Application *application, void *WindowProc);

#define WM_TOGGLEFULLSCREEN (WM_USER + 1)

extern bool g_isProgramLooping;
extern bool g_createFullScreen;

#endif

Moreover, I use a file called NeHeGL.h:

#ifndef GL_FRAMEWORK__INCLUDED
#define GL_FRAMEWORK__INCLUDED

#ifndef CDS_FULLSCREEN
#define CDS_FULLSCREEN 4
#endif

#include <windows.h>

class Keys
{
public:
	BOOL keyDown [256];
};

class Application
{
public:
	HINSTANCE			hInstance;
	const char			*className;
};

class GL_WindowInit
{
public:
	Application			*application;
	char				*title;
	int					width;
	int					height;
	int					bitsPerPixel;
	BOOL				isFullScreen;
};

#endif

The file WinMain.cpp also comes from NeHe. It basically calls the routines I implemented in game.cpp.

#include <windows.h>
#include <GL/gl.h>
#include <GL/glu.h>

#include "game.h"

Game intro;

bool g_isProgramLooping;
bool g_createFullScreen;

LRESULT CALLBACK WindowProc (HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	DWORD		tickCount;

	GL_Window* window = (GL_Window*)(GetWindowLong (hWnd, GWL_USERDATA));

	switch (uMsg)
	{
		case WM_ERASEBKGND:
			return 0;

		case WM_PAINT:
		{
			tickCount = GetTickCount ();
			intro.Update (tickCount, window->lastTickCount);
			window->lastTickCount = tickCount;
			intro.Draw (tickCount);
			SwapBuffers (window->hDC);
		}
		return 0;

		case WM_SYSCOMMAND:
		{
			switch (wParam)
			{
				case SC_SCREENSAVE:
				case SC_MONITORPOWER:
				return 0;
			}
			break;
		}
		return 0;

		case WM_CREATE:
		{
			CREATESTRUCT* creation = (CREATESTRUCT*)(lParam);
			window = (GL_Window*)(creation->lpCreateParams);
			SetWindowLong (hWnd, GWL_USERDATA, (LONG)(window));
		}
		return 0;

		case WM_CLOSE:
			TerminateApplication(window);
		return 0;

		case WM_SIZE:
			switch (wParam)
			{
				case SIZE_MINIMIZED:
					window->isVisible = FALSE;
				return 0;

				case SIZE_MAXIMIZED:
					window->isVisible = TRUE;
					ReshapeGL (LOWORD (lParam), HIWORD (lParam));
				return 0;

				case SIZE_RESTORED:
					window->isVisible = TRUE;
					ReshapeGL (LOWORD (lParam), HIWORD (lParam));
				return 0;
			}
		break;

		case WM_KEYDOWN:
			if ((wParam >= 0) && (wParam <= 255))
			{
				window->keys->keyDown [wParam] = TRUE;
				return 0;
			}
		break;

		case WM_KEYUP:
			if ((wParam >= 0) && (wParam <= 255))
			{
				window->keys->keyDown [wParam] = FALSE;
				return 0;
			}
		break;

		case WM_TOGGLEFULLSCREEN:
			g_createFullScreen = (g_createFullScreen == TRUE) ? FALSE : TRUE;
			PostMessage (hWnd, WM_QUIT, 0, 0);
		break;
	}

	return DefWindowProc (hWnd, uMsg, wParam, lParam);
}

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	Application			application;
	GL_Window			window;
	Keys				keys;
	BOOL				isMessagePumpActive;
	MSG				msg;

	application.className = "OpenGL";
	application.hInstance = hInstance;

	ZeroMemory (&window, sizeof (GL_Window));
	window.keys			= &keys;
	window.init.application		= &application;

	window.init.title		= "Evolution - A Game by Adok (Claus Volko)";

	window.init.width		= 640;
	window.init.height		= 480;
	window.init.bitsPerPixel	= 32;
	window.init.isFullScreen	= TRUE;

	ZeroMemory (&keys, sizeof (Keys));

	if (RegisterWindowClass (&application, WindowProc) == FALSE)
	{
		MessageBox (HWND_DESKTOP, "Error Registering Window Class!", "Error", 
		MB_OK | MB_ICONEXCLAMATION);
		return -1;
	}

	g_isProgramLooping = TRUE;
	g_createFullScreen = (bool) window.init.isFullScreen;
	while (g_isProgramLooping)
	{
		window.init.isFullScreen = (BOOL) g_createFullScreen;
		if (CreateWindowGL (&window) == TRUE)
		{
			if (intro.Initialize (&window, &keys, GetTickCount ()) == false)
				TerminateApplication (&window);
			else
			{
				isMessagePumpActive = TRUE;
				while (isMessagePumpActive == TRUE)
				{
					if (PeekMessage (&msg, window.hWnd, 0, 0, PM_REMOVE) != 0)
					{
						if (msg.message != WM_QUIT)
						{
							DispatchMessage (&msg);
						}
						else
						{
							isMessagePumpActive = FALSE;
						}
					}
					else
					{
						if (window.isVisible == FALSE)
						{
							WaitMessage ();
						}
					}
				}
			}

			intro.Deinitialize ();

			DestroyWindowGL (&window);
		}
		else
		{
			MessageBox (HWND_DESKTOP, "Error Creating OpenGL Window", "Error", 
			MB_OK | MB_ICONEXCLAMATION);
			g_isProgramLooping = FALSE;
		}
	}

	UnregisterClass (application.className, application.hInstance);
	return 0;
}

Next comes code for processing image files, which is especially needed for loading them. The first file in this series is Bitmap.cpp. It is based on code from NeHe, but I modified and extended it in some ways:

#include "Bitmap.h"

bool Bitmap::New (long lWidth, long lHeight)
{
	hdcTemp = CreateCompatibleDC (GetDC (0));
	if (!hdcTemp)
	{
		pPicture->Release ();
		return false;
	}

	actualWidthPixels = lWidth;
	actualHeightPixels = lHeight;
	lWidthPixels = lWidth;
	lHeightPixels = lHeight;

	ResizeImageToClosestPowerOfTwo ();

	BITMAPINFO bi = { 0 };

	bi.bmiHeader.biSize		= sizeof (BITMAPINFOHEADER);
	bi.bmiHeader.biBitCount		= 32;
	bi.bmiHeader.biWidth		= lWidthPixels;
	bi.bmiHeader.biHeight		= lHeightPixels;
	bi.bmiHeader.biCompression	= BI_RGB;
	bi.bmiHeader.biPlanes		= 1;

	hbmpTemp = CreateDIBSection (hdcTemp, &bi, DIB_RGB_COLORS, (void**)&pBits, 0, 0);
	
	if (!hbmpTemp)
	{
		DeleteDC (hdcTemp);
		pPicture->Release ();
		return false;
	}

	transparencyMask = new bool [lWidthPixels * lHeightPixels];

	for (long i = 0; i < lWidthPixels * lHeightPixels; i++)
	{
		BYTE *pPixel	= (BYTE*)(&pBits [i]);
		pPixel [3]		= 0;
		transparencyMask [i]	= true;
	}

	loaded = false;

	return true;
}

bool Bitmap::LoadFileFromMemory (int res, char *name, bool transparency, GLuint *transparentCol)
{
	HRSRC rec;
	HGLOBAL	handle;
	unsigned char *data;
	int length;
	FILE *tempFile;
	
	rec = FindResource (NULL, MAKEINTRESOURCE (res), name);
	handle = LoadResource (NULL, rec);
	
	data = (unsigned char *)LockResource (handle);
	length = SizeofResource (NULL, rec);

	tempFile = fopen ("temp", "wb");

	for (int i = 0; i < length; i++)
		fputc (data [i], tempFile);

	fclose (tempFile);

	return LoadFile ("temp", transparency, transparentCol);
}

bool Bitmap::LoadFile (char *szPathName, bool transparency, GLuint *transparentCol)
{
	OLECHAR		wszPath [MAX_PATH + 1];
	char		szPath [MAX_PATH + 1];
	long		lWidth;
	long		lHeight;
	DWORD		*pBits_tmp;

	if (strstr (szPathName, "http://"))
		strcpy (szPath, szPathName);
	else
	{
		GetCurrentDirectory (MAX_PATH, szPath);
		strcat (szPath, "\\");
		strcat (szPath, szPathName);
	}

	MultiByteToWideChar (CP_ACP, 0, szPath, -1, wszPath, MAX_PATH);
	HRESULT hr = OleLoadPicturePath (wszPath, 0, 0, 0, IID_IPicture, (void**)&pPicture);

	if (FAILED (hr))
		return false;

	hdcTemp = CreateCompatibleDC (GetDC (0));
	if (!hdcTemp)
	{
		pPicture->Release ();
		return false;
	}
	
	pPicture->get_Width (&lWidth);
	lWidthPixels	= MulDiv (lWidth, GetDeviceCaps (hdcTemp, LOGPIXELSX), 2540);
	actualWidthPixels = lWidthPixels;
	pPicture->get_Height (&lHeight);
	lHeightPixels	= MulDiv (lHeight, GetDeviceCaps (hdcTemp, LOGPIXELSY), 2540);
	actualHeightPixels = lHeightPixels;

	ResizeImageToClosestPowerOfTwo ();

	BITMAPINFO bi = { 0 };

	bi.bmiHeader.biSize			= sizeof (BITMAPINFOHEADER);
	bi.bmiHeader.biBitCount		= 32;
	bi.bmiHeader.biWidth		= actualWidthPixels;
	bi.bmiHeader.biHeight		= actualHeightPixels;
	bi.bmiHeader.biCompression	= BI_RGB;
	bi.bmiHeader.biPlanes		= 1;

	hbmpTemp = CreateDIBSection (hdcTemp, &bi, DIB_RGB_COLORS, (void**)&pBits_tmp, 0, 0);
	
	if (!hbmpTemp)
	{
		DeleteDC (hdcTemp);
		pPicture->Release ();
		return false;
	}

	SelectObject (hdcTemp, hbmpTemp);

	pPicture->Render (hdcTemp, 0, 0, actualWidthPixels, actualHeightPixels, 0, 
	lHeight, lWidth, -lHeight, 0);

	bi.bmiHeader.biSize		= sizeof (BITMAPINFOHEADER);
	bi.bmiHeader.biBitCount		= 32;
	bi.bmiHeader.biWidth		= lWidthPixels;
	bi.bmiHeader.biHeight		= lHeightPixels;
	bi.bmiHeader.biCompression	= BI_RGB;
	bi.bmiHeader.biPlanes		= 1;

	hbmpTemp = CreateDIBSection (hdcTemp, &bi, DIB_RGB_COLORS, (void**)&pBits, 0, 0);
	
	if (!hbmpTemp)
	{
		DeleteDC (hdcTemp);
		pPicture->Release ();
		return false;
	}

	transparencyMask = new bool [lWidthPixels * lHeightPixels];

	long i;

	for (i = 0; i < lWidthPixels * lHeightPixels; i++)
	{
		BYTE *pPixel		= (BYTE*)(&pBits [i]);
		pPixel [3]		= 0;
		transparencyMask [i]	= true;
	}

	i = 0;
	long i_tmp = 0;
	for (long y = 0; y < actualHeightPixels; y++)
	{
		for (long x = 0; x < actualWidthPixels; x++)
		{
			BYTE *pPixel			= (BYTE*)(&pBits [i]);
			BYTE *pPixel_tmp		= (BYTE*)(&pBits_tmp [i_tmp]);

			pPixel [0]			= pPixel_tmp [2];
			pPixel [1]			= pPixel_tmp [1];
			pPixel [2]			= pPixel_tmp [0];

			if ((transparency && pPixel [0] == transparentCol [0] 
			&& pPixel [1] == transparentCol [1] && pPixel [2] == transparentCol [2]))
			{
				pPixel [3]				=   0;
				transparencyMask [i]	= true;
			}
			else
			{
				pPixel [3]				= 255;
				transparencyMask [i]	= false;
			}

			i++;
			i_tmp++;
		}
		i += lWidthPixels - actualWidthPixels;
	}

	loaded = true;

	return true;
}

bool Bitmap::LoadFile (char *szPathName)
{
	return LoadFile (szPathName, false, NULL);
}

void Bitmap::RenderText (TextObject *textObject, AlignmentTypes align)
{
	HBITMAP hbmpText;

	SelectObject (hdcTemp, hFont);

	BITMAPINFO	biText = {0};
	DWORD		*pBitsText = 0;

	biText.bmiHeader.biSize			= sizeof (BITMAPINFOHEADER);
	biText.bmiHeader.biBitCount		= 32;
	biText.bmiHeader.biWidth		= lWidthPixels;
	biText.bmiHeader.biHeight		= lHeightPixels;
	biText.bmiHeader.biCompression	= BI_RGB;
	biText.bmiHeader.biPlanes		= 1;

	hbmpText = CreateDIBSection (hdcTemp, &biText, DIB_RGB_COLORS, (void**)&pBitsText, 0, 0);

	SelectObject (hdcTemp, hbmpText);

	RECT rect;
	rect.top = 0;
	rect.left = 0;
	rect.bottom = lHeightPixels - 1;
	rect.right = lWidthPixels - 1;
	textObject->DrawFormatted (hdcTemp, &rect, align);

	for (long i = 0; i < lWidthPixels * lHeightPixels; i++)
	{
		BYTE *pPixelText = (BYTE*)(&pBitsText [i]);
		BYTE *pPixel	= (BYTE*)(&pBits [i]);

		if (!(pPixelText [0] == 255 && pPixelText [1] == 255 && pPixelText[2] == 255)
			&& !(pPixelText [0] == 0 && pPixelText [1] == 0 && pPixelText[2] == 0))
		{
			pPixel [0]				= pPixelText [0];
			pPixel [1]				= pPixelText [1];
			pPixel [2]				= pPixelText [2];
			pPixel [3]				= 255;
			transparencyMask [i]	= false;
		}
	}

	DeleteObject (hbmpText);
}

void Bitmap::GenerateTexture (GLuint &texid)
{
	glGenTextures (1, &texid);
	glBindTexture (GL_TEXTURE_2D, texid);
	glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	UpdateTexture (texid);
}

void Bitmap::UpdateTexture (GLuint &texid)
{
	glBindTexture (GL_TEXTURE_2D, texid);
	glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA, lWidthPixels, lHeightPixels, 0, 
	GL_RGBA, GL_UNSIGNED_BYTE, pBits);
}

void Bitmap::Delete ()
{
	DeleteObject (hbmpTemp);
	DeleteDC (hdcTemp);

	if (loaded)
		pPicture->Release();
}

HDC Bitmap::GetHdcTemp ()
{
	return hdcTemp;
}

void Bitmap::ChangeOpacity (int newOpacity)
{
	for (long i = 0; i < lWidthPixels * lHeightPixels; i++)
	{
		BYTE *pPixel = (BYTE*)(&pBits [i]);

		if (!transparencyMask [i])
			pPixel [3] = newOpacity;
	}
}

void Bitmap::PutPixel (int x, int y, int *color)
{
	BYTE *pPixel = (BYTE *)(&pBits [y * lWidthPixels + x]);
	for (int i = 0; i < 4; i++)
		pPixel [i] = color [i];
	transparencyMask [y * lWidthPixels + x] = false;
}

void Bitmap::SetPixel (int x, int y, int *color)
{
	PutPixel (x, y, color);
}

int *Bitmap::GetPixel (int x, int y)
{
	BYTE *pPixel = (BYTE *)(&pBits [y * lWidthPixels + x]);
	int *color = new int [4];
	for (int i = 0; i < 4; i++)
		color [i] = pPixel [i];
	return color;
}

void Bitmap::SetRenderTextColor (COLORREF color)
{
	SetTextColor (hdcTemp, color);
}

void Bitmap::SetRenderTextFont (char *face, int height, bool italic, bool underline)
{
	hFont = CreateFontA (height, 0, 0, 0, 0, italic, underline, 0, 'w', 
		    OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, 
			DEFAULT_PITCH | FF_DONTCARE, face);
}

void Bitmap::SetRenderTextFont (char *face, int height)
{
	SetRenderTextFont (face, height, false, false);
}

DWORD *Bitmap::GetPBits ()
{
	return pBits;
}

long Bitmap::GetLHeightPixels ()
{
	return lHeightPixels;
}

long Bitmap::GetLWidthPixels ()
{
	return lWidthPixels;
}

long Bitmap::GetActualHeightPixels ()
{
	return actualHeightPixels;
}

long Bitmap::GetActualWidthPixels ()
{
	return actualWidthPixels;
}

void Bitmap::Copy (Bitmap source)
{
	int x0 = (actualWidthPixels - source.GetActualWidthPixels ()) >> 1;
	int y0 = lHeightPixels - actualHeightPixels + 
	((actualHeightPixels - source.GetActualHeightPixels ()) >> 1);
	DWORD *pBits_src = source.GetPBits () + (source.GetLHeightPixels () 
	- source.GetActualHeightPixels ()) * source.GetLWidthPixels ();
	DWORD *pBits_dst = pBits + y0 * lWidthPixels + x0;
	bool *transparencyMask_dst = transparencyMask + y0 * lWidthPixels + x0;

	for (int y = 0; y < source.GetActualHeightPixels (); y++)
	{
		for (int x = 0; x < source.GetActualWidthPixels (); x++)
		{
			*pBits_dst = *pBits_src;
			*transparencyMask_dst = false;
			pBits_src++;
			pBits_dst++;
			transparencyMask_dst++;
		}
		pBits_src += source.GetLWidthPixels () - source.GetActualWidthPixels ();
		pBits_dst += lWidthPixels - source.GetActualWidthPixels ();
		transparencyMask_dst += lWidthPixels - source.GetActualWidthPixels ();
	}
}

void Bitmap::Clear (int *color)
{
	if (color == NULL)
		for (int i = 0; i < lWidthPixels * lHeightPixels; i++)
		{
			pBits [i] = 0;
			transparencyMask [i] = true;
		}
	else
		for (int i = 0; i < lWidthPixels * lHeightPixels; i++)
		{
			pBits [i] = (((((color [3] << 8) + color [2]) << 8) 
			+ color [1]) << 8) + color [0];
			transparencyMask [i] = true;
		}
}

void Bitmap::SplitVertical (int splitY, Bitmap *bitmap1, Bitmap *bitmap2)
{
	DWORD *pBits_src = pBits;
	DWORD *pBits_dst;
	int y;
	bool *transparencyMask_dst;
	int lWidthPixels_dst;

	bitmap1->New (actualWidthPixels, splitY);
	bitmap2->New (actualWidthPixels, actualHeightPixels - splitY);
	bitmap1->Clear ();
	bitmap2->Clear ();

	pBits_dst = bitmap1->GetPBits ();
	transparencyMask_dst = bitmap1->GetTransparencyMask ();
	lWidthPixels_dst = bitmap1->GetLWidthPixels ();

	for (y = 0; y < actualHeightPixels; y++)
	{
		if (y == splitY)
		{
			pBits_dst = bitmap2->GetPBits ();
			transparencyMask_dst = bitmap2->GetTransparencyMask ();
			lWidthPixels_dst = bitmap2->GetLWidthPixels ();
		}

		for (int x = 0; x < actualWidthPixels; x++)
		{
			*pBits_dst = *pBits_src;
			*transparencyMask_dst = false;
			pBits_src++;
			pBits_dst++;
			transparencyMask_dst++;
		}
		pBits_src += lWidthPixels - actualWidthPixels;
		pBits_dst += lWidthPixels_dst - actualWidthPixels;
		transparencyMask_dst += lWidthPixels_dst - actualWidthPixels;
	}
}

void Bitmap::ResizeImageToClosestPowerOfTwo ()
{
	GLint glMaxTexDim;

	glGetIntegerv (GL_MAX_TEXTURE_SIZE, &glMaxTexDim);

	if (lWidthPixels <= glMaxTexDim)
		lWidthPixels = ClosestPowerOfTwo (lWidthPixels);
	else
		lWidthPixels = glMaxTexDim;
 
	if (lHeightPixels <= glMaxTexDim)
		lHeightPixels = ClosestPowerOfTwo (lHeightPixels);
	else
		lHeightPixels = glMaxTexDim;
}

long Bitmap::ClosestPowerOfTwo (long v)
{
	bool breakcond = false;

	for (unsigned long i = 1 << ((sizeof (long) << 3) - 1); i && !breakcond; i >>= 1)
	{
		if (v & i)
		{
			if (v != i)
				v = i << 1;
			breakcond = true;
		}
	}

	return v;
}

bool *Bitmap::GetTransparencyMask ()
{
	return transparencyMask;
}

There is also an accompanying Bitmap.h:

#ifndef _BITMAP_H_
#define _BITMAP_H_

#include <stdio.h>
#include <windows.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <olectl.h>
#include <math.h>
#include "TextObject.h"
#include "AlignmentTypes.h"

class Bitmap
{
public:
	bool New (long lWidth, long lHeight);
	bool LoadFileFromMemory (int res, char *name, bool transparency, GLuint *transparentCol);
	bool LoadFile (char *szPathName, bool transparency, GLuint *transparentCol);
	bool LoadFile (char *szPathName);
	void RenderText (TextObject *textObject, AlignmentTypes align = Align_Left);
	void GenerateTexture (GLuint &texid);
	void UpdateTexture (GLuint &texid);
	void Delete ();
	HDC GetHdcTemp ();
	void ChangeOpacity (int newOpacity);
	void PutPixel (int x, int y, int *color);
	void SetPixel (int x, int y, int *color);
	int *GetPixel (int x, int y);
	void SetRenderTextColor (COLORREF color);
	void SetRenderTextFont (char *face, int height, bool italic, bool underline);
	void SetRenderTextFont (char *face, int height);
	DWORD *GetPBits ();
	void Copy (Bitmap source);
	long GetLHeightPixels ();
	long GetLWidthPixels ();
	long GetActualHeightPixels ();
	long GetActualWidthPixels ();
	void Clear (int *color = NULL);
	void SplitVertical (int splitY, Bitmap *bitmap1, Bitmap *bitmap2);
	void ResizeImageToClosestPowerOfTwo ();
	long ClosestPowerOfTwo (long v);
	bool *GetTransparencyMask ();

private:
	HDC hdcTemp;
	HBITMAP hbmpTemp;
	long lWidthPixels;
	long lHeightPixels;
	long actualWidthPixels;
	long actualHeightPixels;
	DWORD *pBits;
	bool *transparencyMask;
	IPicture *pPicture;
	bool loaded;
	HFONT hFont;
};

#endif

For text output I use a modified variant of some piece of code I found in Charles Petzold's book on Windows programming. It consists of TextObject.cpp and TextObject.h. Here is TextObject.cpp:

#include "TextObject.h"

void TextObject::RemoveChars (PTSTR pBegin, PTSTR pEnd)
{
	do
	{
		*pBegin = *pEnd;
		pBegin++;
		pEnd++;
	}
	while (*(pEnd - 1) != '\0');
}

int TextObject::ProcessTabsAndLineBreaks (PTSTR pText)
{
	bool loop = true;
	int cSpaceChars = 0;

	while (loop)
	{
		loop = false;

		if (*pText == '\t' || *pText == '\n' || *pText == '\r')
		{
			if (cSpaceChars == 0)
			{
				*pText = ' ';
				pText++;
				cSpaceChars++;
			}
			else
				RemoveChars (pText, pText + 1);
			loop = true;
		}
	}

	return cSpaceChars;
}

void TextObject::PreprocessText ()
{
	int cSpaceChars;
	PTSTR pText, pPrevious;

	pText = text;

	while (*pText != '\0')
	{
		pPrevious = pText;
		cSpaceChars = 0;

		while (*pText == ' ')
		{
			cSpaceChars++;
			pText++;
		}

		if (cSpaceChars > 1)
			RemoveChars (pPrevious + 1, pText);

		cSpaceChars += ProcessTabsAndLineBreaks (pText);

		pText++;
	}
}

void TextObject::DrawFormatted (HDC hdc, RECT *prc, AlignmentTypes align)
{
	int		xStart, yStart, cSpaceChars;
	PTSTR	pBegin, pEnd, pText;
	SIZE	size;

	pText = text;
	yStart = prc->top;

	do
	{
		cSpaceChars = 0;
		
		while (*pText == ' ')
			pText++;

		pBegin = pText;

		do
		{
			pEnd = pText;
			
			while (*pText != '\0' && *pText != ' ')
				pText++;

			if (*pText == ' ')
			{
				pText++;
				cSpaceChars++;
				GetTextExtentPoint32A (hdc, pBegin, pText - pBegin, &size);
			}
		} while (*pText != '\0' && size.cx < (prc->right - prc->left));

		if (*pText != '\0')
			cSpaceChars--;

		while (*(pEnd - 1) == ' ')
		{
			pEnd--;
			cSpaceChars--;
		}

		if (cSpaceChars < 0 && *pText != '\0' && size.cx >= (prc->right - prc->left))
		{
			pText = pBegin;

			cSpaceChars = 0;

			do
			{
				pEnd = pText;
			
				pText++;
				GetTextExtentPoint32A (hdc, pBegin, pText - pBegin, &size);

				if (*pText == ' ')
					cSpaceChars++;
			} while (size.cx < (prc->right - prc->left));
		}

		if (*pText == '\0')
		{
			GetTextExtentPoint32 (hdc, pBegin, pText - pBegin - 1, &size);
			if (size.cx < (prc->right - prc->left))
				pEnd = pText;
		}

		GetTextExtentPoint32 (hdc, pBegin, pEnd - pBegin, &size);

		switch (align)
		{
		case Align_Left:
			xStart = prc->left;
			break;

		case Align_Right:
			xStart = prc->right - size.cx;
			break;

		case Align_Center:
			xStart = (prc->right + prc->left - size.cx) / 2;
			break;

		case Align_Justify:
			if (*pEnd != '\0' && cSpaceChars > 0)
				SetTextJustification (hdc, prc->right - prc->left - size.cx, cSpaceChars);
			xStart = prc->left;
			break;
		}

		TextOut (hdc, xStart, yStart, pBegin, pEnd - pBegin);

		SetTextJustification (hdc, 0, 0);
		yStart += size.cy;
		pText = pEnd;
	}
	while (*pText && yStart < prc->bottom - size.cy);
}

void TextObject::SetText (char *src)
{
	text = new char [strlen (src) + 1];
	strcpy (text, src);
}

This is the header file TextObject.h:

#ifndef _TEXTOBJECT_H_
#define _TEXTOBJECT_H_

#include <windows.h>
#include "AlignmentTypes.h"

class TextObject
{
public:
	void PreprocessText ();
	void DrawFormatted (HDC hdc, RECT *prc, AlignmentTypes align);
	void SetText (char *src);

private:
	void RemoveChars (PTSTR pBegin, PTSTR pEnd);
	int ProcessTabsAndLineBreaks (PTSTR pText);
	char *text;
};

#endif

Finally, there is the header file AlignmentTypes.h with some trivial definitions for text formatting. This completes the base code.

#ifndef _ALIGNMENTTYPES_H_
#define _ALIGNMENTTYPES_H_

enum AlignmentTypes
{
	Align_Left,
	Align_Right,
	Align_Center,
	Align_Justify
};

#endif

The Actual Game Code

Let's start with game.h, which contains declarations of variables, methods and data types used in game.cpp.

#ifndef _GAME_H_
#define _GAME_H_

#include <windows.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <stdio.h>

#include "NeHeGL.h"
#include "NeHe_Window.h"
#include "Bitmap.h"
#include "bass.h"

class Position
{
public:
	int x, y;
};

enum EnemyType
{
	Kraken,
	Seastar,
	Jellyfish,
	Cancer,
	EnemyBullet,
	CancerBullet,
	Lion,
	Worm
};

class Enemy
{
public:
	Position position;
	EnemyType type;
	int width, height;
	bool moving;
	Position whereToGo;
	int variable [10];
};

class Game
{
public:
	bool Initialize (GL_Window *window, Keys *keys, float tickCount);
	void Deinitialize ();
	void Update (float tickCount, float lastTickCount);
	void Draw (float tickCount);

private:
	void NewGame (float tickCount);
	void UpdateLives (bool generate);
	void NewGame_Sub (float tickCount);
	void EnterDinosaur (float tickCount);

	enum GameStates
	{
		ActualGame,
		GameOver,
		TitlePicture,
		ActualGame_Dinosaur,
		GameCompleted
	} gameState;

	GL_Window *g_window;
	Keys *g_keys;
 	bool spacePressed;
	int spacePressedSince;
	bool escapePressed;
	bool keyPressedUpDown;
	bool keyPressedLeftRight;
	int keyPressedUpDownSince;
	int keyPressedLeftRightSince;
	int blinkingDuration;
	Bitmap bitmap [20];
	GLuint texture [20];
	Position playerPosition;
	(Position bulletPosition) [10];
	bool bulletActive [10];
	int lastBulletMove;
	(Enemy enemy) [20];
	int lastEnemyAppeared;
	bool enemyActive [20];
	int startTime;
	int lives;
	Bitmap bitmap_lives;
	GLuint texture_lives;
	Bitmap bitmap_titlescreen;
	GLuint texture_titlescreen;
	Bitmap bitmap_gameover;
	GLuint texture_gameover;
	(Position platformPosition) [15];
	bool platformActive [15];
	bool dinosaurMoving;
	int jumping;
	int falling;
	int movingswitchedSince;
	bool movingDirection;
	bool gap;
	int rightmost;
	bool bulletDirection [10];
	int completed;
	Bitmap bitmap_gamecompleted;
	GLuint texture_gamecompleted;
	bool scrolling;
	int upward;
	int lastVerticalMove;
	HSTREAM soundeffect [6];
};

#endif

Let us just take a glance at some parts of the previous code.

class Enemy
{
public:
	Position position;
	EnemyType type;
	int width, height;
	bool moving;
	Position whereToGo;
	int variable [10];
};

This defines the data structure which is used to store the data needed to describe an enemy appearing in the game. Instead of a class, we could have defined it as a struct. A struct is actually just a special case of a class in which all the variables are public and there are no methods. But this is just a matter of syntax. The position consists of x and y coordinate, and the type of the enemy makes use of the previously defined enumeration EnemyType. The variable whereToGo will be used in different ways depending on the type of the enemy, as we will see. Moreover, ten extra variables are declared for each enemy, although we will only make use of a few of them. They are supposed to be a reserve. Maybe you will need them if you extend the game.

	(Position bulletPosition) [10];

The idea behind this and some analogous declarations is that we can display up to ten bullets at once. If a bullet is supposed to be put onto screen, the program will check if a "slot" is available (i.e. one bulletActive is false). If so, the bullet will be created, otherwise not. As soon as a bullet moves out of sight, the corresponding bulletActive will be set to false so we have another empty "slot" for a new bullet.

Now I will give you the code of the last but most important file, which is game.cpp. Instead of writing it down once, I will pause several times to explain important parts of the program logic.

#include "game.h"

bool Game::Initialize (GL_Window* window, Keys* keys, float tickCount)	
{
  int i;

  g_window			= window;
  g_keys	  		= keys;

  glEnable (GL_POINT_SMOOTH);
  glHint (GL_POINT_SMOOTH_HINT, GL_NICEST);
  glEnable (GL_LINE_SMOOTH);
  glHint (GL_LINE_SMOOTH_HINT, GL_NICEST);

  glClearColor (0, 0.0, 0.3f, 1);
  glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glMatrixMode (GL_PROJECTION);
  glLoadIdentity ();
  gluOrtho2D (0.0, 640.0, 0.0, 480.0);

  glAlphaFunc (GL_GREATER, 0.1f);
  glEnable (GL_ALPHA_TEST);
  glBlendFunc (GL_SRC_ALPHA, GL_ONE);

  GLuint color [3] = {0, 0, 0};

  if (!bitmap [0].LoadFile ("fish.bmp", true, color)) return false;
  if (!bitmap [1].LoadFile ("bullet.bmp", true, color)) return false;
  if (!bitmap [2].LoadFile ("enemy_bullet.bmp", true, color)) return false;
  if (!bitmap [3].LoadFile ("enemy_cancer.bmp", true, color)) return false;
  if (!bitmap [4].LoadFile ("enemy_cancerbullet.bmp", true, color)) return false;
  if (!bitmap [5].LoadFile ("enemy_jellyfish.bmp", true, color)) return false;
  if (!bitmap [6].LoadFile ("enemy_kraken.bmp", true, color)) return false;
  if (!bitmap [7].LoadFile ("enemy_seastar.bmp", true, color)) return false;
  if (!bitmap [8].LoadFile ("platform.bmp", true, color)) return false;
  if (!bitmap [9].LoadFile ("dino_stand.bmp", true, color)) return false;
  if (!bitmap [10].LoadFile ("dino_move.bmp", true, color)) return false;
  if (!bitmap [11].LoadFile ("dino_bullet.bmp", true, color)) return false;
  if (!bitmap [12].LoadFile ("enemy_bulletdino.bmp", true, color)) return false;
  if (!bitmap [13].LoadFile ("enemy_lion.bmp", true, color)) return false;
  if (!bitmap [14].LoadFile ("enemy_worm.bmp", true, color)) return false;
  if (!bitmap_titlescreen.LoadFile ("titlescreen.bmp", true, color)) return false;
  if (!bitmap_gameover.LoadFile ("gameover.bmp", true, color)) return false;
  if (!bitmap_gamecompleted.LoadFile ("gamecompleted.bmp", true, color)) return false;
  if (!bitmap_lives.New (64, 16)) return false;

  bitmap_titlescreen.GenerateTexture (texture_titlescreen);
  bitmap_gameover.GenerateTexture (texture_gameover);
  bitmap_gamecompleted.GenerateTexture (texture_gamecompleted);
  for (i = 0; i < 20; i++)
    bitmap [i].GenerateTexture (texture [i]);

  BASS_Init (-1, 44100, 0, NULL, NULL);
  soundeffect [0] = BASS_StreamCreateFile (FALSE, "enemy_shoot.wav", 0, 0, 0);
  soundeffect [1] = BASS_StreamCreateFile (FALSE, "gameover.wav", 0, 0, 0);
  soundeffect [2] = BASS_StreamCreateFile (FALSE, "hit.wav", 0, 0, 0);
  soundeffect [3] = BASS_StreamCreateFile (FALSE, "jump.wav", 0, 0, 0);
  soundeffect [4] = BASS_StreamCreateFile (FALSE, "loselife.wav", 0, 0, 0);
  soundeffect [5] = BASS_StreamCreateFile (FALSE, "shoot.wav", 0, 0, 0);

  srand (tickCount);
  spacePressed = false;
  gameState = TitlePicture;

  return true;
}

The code so far just sets OpenGL up and loads files needed in the game.

void Game::NewGame (float tickCount)
{
  NewGame_Sub (tickCount);
  glClearColor (0, 0, 0.3f, 1);
  playerPosition.x = 20;
  playerPosition.y = 200;
  lives = 5;
  UpdateLives (true);
  gameState = ActualGame;
}

This function is called whenever a new game starts in regular mode, i.e. when no cheat is used. The game starts in fish mode, which is the 2D shooter part.

void Game::EnterDinosaur (float tickCount)
{
  int i;

  NewGame_Sub (tickCount);
  glClearColor (0, 0, 0.6f, 1);
  for (i = 0; i < 10; i++)
  {
    platformPosition [i].x = i * 64;
    platformPosition [i].y = 0;
    platformActive [i] = true;
  }
  for (; i < 15; i++)
    platformActive [i] = false;
  playerPosition.x = 20;
  playerPosition.y = 16;
  dinosaurMoving = false;
  jumping = 0;
  falling = 0;
  movingswitchedSince = 0;
  movingDirection = true;
  rightmost = 9;
  gap = false;
  completed = 0;
  scrolling = false;
  upward = 0;
  lastVerticalMove = 0;
  gameState = ActualGame_Dinosaur;
}

This function is called when the player reaches dinosaur mode, the platform part.

void Game::NewGame_Sub (float tickCount)
{
  int i;

  for (i = 0; i < 10; i++)
    bulletActive [i] = false;
  for (i = 0; i < 20; i++)
    enemyActive [i] = false;
  lastBulletMove = 0;
  keyPressedLeftRightSince = 0;
  keyPressedUpDownSince = 0;
  spacePressedSince = 0;
  spacePressed = false;
  escapePressed = false;
  keyPressedLeftRight = false;
  keyPressedUpDown = false;
  lastEnemyAppeared = 0;
  startTime = tickCount;
}

This is a subfunction used by the two previous methods.

void Game::Deinitialize ()
{
}

void Game::Update (float tickCount, float lastTickCount)
{

The method Update is called during the main loop of the game. The parameters tickCount and lastTickCount show us how much time has elapsed since the last call of this method.

  int i, j;

  if (escapePressed && !g_keys->keyDown [VK_ESCAPE])
  {
    if (gameState == TitlePicture)
      exit (1);
    else if (gameState == ActualGame)
    {
      glClearColor (0, 0, 0.3f, 1);
      escapePressed = false;
      spacePressed = false;
      gameState = GameOver;
      BASS_ChannelPlay (soundeffect [1], TRUE);
    }
    else
    {
      glClearColor (0, 0, 0.3f, 1);
      escapePressed = false;
      spacePressed = false;
      gameState = TitlePicture;
    }
  }
  else if (!escapePressed && g_keys->keyDown [VK_ESCAPE])
    escapePressed = true;

  if (gameState == GameOver)
  {
    if (!spacePressed && g_keys->keyDown [VK_SPACE])
      spacePressed = true;

    if (spacePressed && !g_keys->keyDown [VK_SPACE])
    {
      glClearColor (0, 0, 0.3f, 1);
      spacePressed = false;
      gameState = TitlePicture;
      return;
    }
  }

  if (gameState == TitlePicture)
  {
    if (!spacePressed && g_keys->keyDown [VK_SPACE])
      spacePressed = true;

    if (spacePressed && !g_keys->keyDown [VK_SPACE])
    {
      NewGame (tickCount);
      return;
    }

    // cheat: 300 lives
    if (g_keys->keyDown [VK_F9])
    {
      NewGame (tickCount);
      lives = 300;
      UpdateLives (false);
      return;
    }

    // cheat to skip fish mode
    if (g_keys->keyDown [VK_F7])
    {
      lives = 5;
      UpdateLives (true);
      EnterDinosaur (tickCount);
      return;
    }
  }

This is code for handling part of the input, such as quitting the game. Note that the game sometimes does not react on the keypress, but on the release of a (previously pressed) key. This is necessary in some cases, otherwise parts of the game would be skipped.

As you can see here there are also two cheat modes. Pressing F9 when the title screen is displayed will give you 300 lives, and pressing F7 will make you start in dinosaur mode. In fact the game is very hard and I know nobody who managed to complete the fish part with just 5 lives so these cheats give the player an opportunity to explore the dinosaur part without having to survive as a fish first.

  if (gameState == ActualGame)
  {
    if (tickCount - startTime > 300000)
    {
      EnterDinosaur (tickCount);
      return;
    }

Once the player has survived 5 minutes as a fish, the dinosaur part will begin.

    if (g_keys->keyDown [VK_DOWN])
    {
      if (!keyPressedUpDown || keyPressedUpDownSince + 10 < tickCount)
      {
        keyPressedUpDown = true;
        keyPressedUpDownSince = tickCount;
        if (playerPosition.y > 70)
          playerPosition.y -= 3;
      }
    }
    else if (g_keys->keyDown [VK_UP])
    {
      if (!keyPressedUpDown || keyPressedUpDownSince + 10 < tickCount)
      {
        keyPressedUpDown = true;
        keyPressedUpDownSince = tickCount;
        if (playerPosition.y < 416)
          playerPosition.y += 3;
      }
    }
    else if (!g_keys->keyDown [VK_DOWN] && !g_keys->keyDown [VK_UP])
      keyPressedUpDown = false;

    if (g_keys->keyDown [VK_LEFT])
    {
      if (!keyPressedLeftRight || keyPressedLeftRightSince + 10 < tickCount)
      {
        keyPressedLeftRight = true;
        keyPressedLeftRightSince = tickCount;
        if (playerPosition.x > 2)
          playerPosition.x -= 3;
      }
    }
    else if (g_keys->keyDown [VK_RIGHT])
    {
      if (!keyPressedLeftRight || keyPressedLeftRightSince + 10 < tickCount)
      {
        keyPressedLeftRight = true;
        keyPressedLeftRightSince = tickCount;
        if (playerPosition.x < 400)
          playerPosition.x += 3;
      }
    }
    else if (!g_keys->keyDown [VK_LEFT] && !g_keys->keyDown [VK_RIGHT])
      keyPressedLeftRight = false;

These are the basic controls of the fish part (mode ActualGame). In a 2D shooter controls are usually very simple. The cursor keys will move you around the screen. Of course there has to be a delay, controlled by tickCount and variables such as keyPressedUpDownSince, otherwise the speed of the fish would depend on the speed of the PC (and most likely be far too high). Of course you also have to check if the character remains in a given rectangle.

    if (g_keys->keyDown [VK_SPACE])
    {
      if (spacePressedSince + 800 < tickCount)
      {
        spacePressed = true;
        spacePressedSince = tickCount;
        for (i = 0; i < 10 && bulletActive [i]; i++);
        if (i < 10)
        {
          bulletPosition [i].x = playerPosition.x + 64;
          bulletPosition [i].y = playerPosition.y;
          bulletActive [i] = true;
          BASS_ChannelPlay (soundeffect [5], TRUE);
        }
      }
    }
    else
      spacePressed = false;

With the Space key, you shoot bullets. As described in the section about game.h, bullets appear on the screen only if there is an empty "slot".

    if (lastBulletMove + 5 < tickCount)
    {
      lastBulletMove = tickCount;
      for (i = 0; i < 10; i++)
        if (bulletActive [i])
        {
          bulletPosition [i].x += 5;
          if (bulletPosition [i].x >= 640)
            bulletActive [i] = false;
        }

The variable lastBulletMove stores the tick count that was when the bullets moved the last time. With a delay of 5 ticks, the active bullets move to the right by 5 pixels. If they reach the screen border, they are deactivated.

      for (i = 0; i < 20; i++)
        if (enemyActive [i])
        {
          if (playerPosition.x + 64 >= enemy [i].position.x
            && playerPosition.x <= enemy [i].position.x + enemy [i].width
            && playerPosition.y + 32 >= enemy [i].position.y
            && playerPosition.y <= enemy [i].position.y + enemy [i].height)
          {
            lives--;
            UpdateLives (false);
            enemyActive [i] = false;
            BASS_ChannelPlay (soundeffect [4], TRUE);
          }

This checks if the player collides with an enemy. If so, he or she loses one life, and the enemy is removed.

          for (j = 0; j < 10 && enemyActive [i]; j++)
              if (bulletActive [j]
                && bulletPosition [j].x + 64 >= enemy [i].position.x
                && bulletPosition [j].x <= enemy [i].position.x + enemy [i].width
                && bulletPosition [j].y + 32 >= enemy [i].position.y
                && bulletPosition [j].y <= enemy [i].position.y + enemy [i].height)
              {
                bulletActive [j] = false;

                if (enemy [i].type == Jellyfish || enemy [i].type == Kraken 
                  || enemy [i].type == EnemyBullet || enemy [i].type == CancerBullet)
                {
                  BASS_ChannelPlay (soundeffect [2], TRUE);
                  enemyActive [i] = false;
                }
              }

This is to check whether a bullet has hit an enemy.

          if (enemyActive [i])
          {
            enemy [i].position.x -= 3;
            if (enemy [i].position.x < -enemy [i].width)
              enemyActive [i] = false;

Since it is a side-scrolling game, enemies keep moving to the left (suggesting that the player is swimming to the right). If an enemy is too far to the left, it will be deactivated to create an empty slot for a new enemy to appear.

            else
            {
              switch (enemy [i].type)
              {
              case Kraken:
                if (enemy [i].moving)
                {
                  if (enemy [i].position.y < enemy [i].whereToGo.y)
                    enemy [i].position.y++;
                  else if (enemy [i].position.y > enemy [i].whereToGo.y)
                    enemy [i].position.y--;

                  if (enemy [i].position.y == enemy [i].whereToGo.y)
                    enemy [i].moving = false;
                }
                else
                {
                  enemy [i].moving = true;
                  if (enemy [i].variable [0] == 0)
                  {
                    enemy [i].whereToGo.y = enemy [i].position.y - 20;
                    enemy [i].variable [0] = 1;
                  }
                  else
                  {
                    enemy [i].whereToGo.y = enemy [i].position.y + 20;
                    enemy [i].variable [0] = 0;
                  }
                }

Here the code controling the behaviour of the enemies commences. It is specific for each type of enemy, and this is the code for the kraken. It alternately moves some pixels upwards and, once it has reached a certain y coordinate, downwards. This is implemented by the code above.

                if (enemy [i].variable [1] + 1000 < tickCount)
                {
                  enemy [i].variable [1] = tickCount;
                  if (rand () % 2)
                  {
                    for (j = 0; j < 20 && enemyActive [j]; j++);
                    if (j < 20)
                    {
                      BASS_ChannelPlay (soundeffect [0], TRUE);
                      enemy [j].type = EnemyBullet;
                      enemy [j].width = 64;
                      enemy [j].height = 32;
                      enemy [j].position.x = enemy [i].position.x - 64 - 1;
                      enemy [j].position.y = enemy [i].position.y + 48;
                      enemyActive [j] = true;
                    }
                  }
                }
                break;

Every 1000 milliseconds the kraken may fire a bullet. The behaviour of the enemy bullet is defined by the following code:

              case EnemyBullet:
                enemy [i].position.x -= 5;
                for (j = 0; j < 20 && enemyActive [i]; j++)
                  if (enemyActive [j] && j != i
                    && enemy [i].position.x + enemy [i].width >= enemy [j].position.x
                    && enemy [i].position.x <= enemy [j].position.x + enemy [j].width
                    && enemy [i].position.y + enemy [i].height >= enemy [j].position.y
                    && enemy [i].position.y <= enemy [j].position.y + enemy [j].height)
                    enemyActive [i] = false;
                break;

This is followed by the behaviour of the jellyfish.

              case Jellyfish:
                if (enemy [i].moving)
                {
                  if (enemy [i].position.y < enemy [i].whereToGo.y)
                  {
                    enemy [i].position.y += 4;
                    if (enemy [i].position.y > enemy [i].whereToGo.y)
                      enemy [i].position.y = enemy [i].whereToGo.y;
                  }
                  else if (enemy [i].position.y > enemy [i].whereToGo.y)
                  {
                    enemy [i].position.y -= 4;
                    if (enemy [i].position.y < enemy [i].whereToGo.y)
                      enemy [i].position.y = enemy [i].whereToGo.y;
                  }

                  if (enemy [i].position.y == enemy [i].whereToGo.y)
                    enemy [i].moving = false;
                }
                else
                {
                  enemy [i].moving = true;
                  if (enemy [i].variable [0] == 0)
                  {
                    enemy [i].whereToGo.y = enemy [i].position.y - 100 - rand () % 200;
                    if (enemy [i].whereToGo.y < enemy [i].height)
                      enemy [i].whereToGo.y = enemy [i].height;
                    enemy [i].variable [0] = 1;
                  }
                  else
                  {
                    enemy [i].whereToGo.y = enemy [i].position.y + 100 - rand () % 200;
                    if (enemy [i].whereToGo.y > 480 - enemy [i].height)
                      enemy [i].whereToGo.y = 480 - enemy [i].height;
                    enemy [i].variable [0] = 0;
                  }
                }
              break;

The jellyfish is similar to the kraken except it does not shoot, and the coordinates to which it moves keep changing so that its movements are far less foreseeable.

Here's the crab:

              case Cancer:
                if (enemy [i].variable [1] + 800 < tickCount)
                {
                  enemy [i].variable [1] = tickCount;
                  if (rand () % 2)
                  {
                    for (j = 0; j < 20 && enemyActive [j]; j++);
                    if (j < 20)
                    {
                      BASS_ChannelPlay (soundeffect [0], TRUE);
                      enemy [j].type = CancerBullet;
                      enemy [j].width = 32;
                      enemy [j].height = 32;
                      enemy [j].position.x = enemy [i].position.x + 48;
                      enemy [j].position.y = enemy [i].position.y + 64 + 1;
                      enemy [j].whereToGo.x = 1 + rand () % 3;
                      if (rand () % 2)
                        enemy [j].whereToGo.x = -enemy [j].whereToGo.x;
                      enemyActive [j] = true;
                    }
                  }
                }
                break;

The crab shoot bullets. These are controlled by the following code:

              case CancerBullet:
                enemy [i].position.x -= enemy [i].whereToGo.x;
                enemy [i].position.y += 5;
                for (j = 0; j < 20 && enemyActive [i]; j++)
                  if (enemyActive [j] && j != i
                    && enemy [i].position.x + enemy [i].width >= enemy [j].position.x
                    && enemy [i].position.x <= enemy [j].position.x + enemy [j].width
                    && enemy [i].position.y + enemy [i].height >= enemy [j].position.y
                    && enemy [i].position.y <= enemy [j].position.y + enemy [j].height)
                    enemyActive [i] = false;
                break;

Next comes code that checks if enough time has expired for a new enemy to appear and, if so, initializes the enemy.

              }
            }
          }
        }
    }

    if ((lastEnemyAppeared + 3000 < tickCount)
      || (lastEnemyAppeared + 750 - (tickCount - startTime) / 1000 < tickCount && rand () % 2))
    {
      for (i = 0; i < 20 && enemyActive [i]; i++);
      if (i < 20)
      {
        for (j = 0; j < 10; j++)
          enemy [i].variable [j] = 0;
        switch (rand () % 10)
        {
        case 0:
        case 1:
          enemy [i].type = Kraken;
          enemy [i].width = 128;
          enemy [i].height = 128;
          enemy [i].position.y = 128 + rand () % (480 - 128 * 2);
          break;

        case 2:
        case 3:
        case 4:
          enemy [i].type = Jellyfish;
          enemy [i].width = 64;
          enemy [i].height = 128;
          enemy [i].position.y = 128 + rand () % (480 - 128 * 2);
          enemy [i].variable [0] = rand () % 2;
          break;

        case 5:
          enemy [i].type = Cancer;
          enemy [i].width = 128;
          enemy [i].height = 64;
          enemy [i].position.y = 0;
          break;

        default:
          enemy [i].type = Seastar;
          enemy [i].width = 64;
          enemy [i].height = 64;
          enemy [i].position.y = 64 + rand () % (480 - 64 * 2);
          break;
        }
        enemy [i].moving = false;
        enemy [i].position.x = 640;
        enemyActive [i] = true;
        lastEnemyAppeared = tickCount;
      }
    }
  }

This was the part of the Update method that dealt with the fish part. The following code is about the dinosaur part.

  if (gameState == ActualGame_Dinosaur)
  {
    if (completed == 800)
    {
      glClearColor (0, 0, 0.3f, 1);
      gameState = GameCompleted;
      return;
    }

Since the dinosaur part has no autoscrolling, it makes no sense to define a timespan which must be survived to complete the game. Therefore there is a variable called "completed" which is incremented when the screen scrolls. Once it reaches 800, the game is finished and the ending screen is displayed.

    if (tickCount > lastVerticalMove + 10)
    {
      lastVerticalMove = tickCount;
      if (jumping)
      {
        playerPosition.y += jumping;
        jumping--;
        if (!jumping)
          falling = 1;
      }

This is the code for jumping. When I was young and tried to figure out how jumping works in a game, my first idea was that some trigonometric function (sine, cosine) was used. But then I realized it is far easier. You simply set up a variable the value of which you add to the position of the player at each frame. In each frame you decrease it and once it is negative, the player will fall. In this way you get a very smooth jump.

      else
      {
        keyPressedUpDown = true;

        if (!falling)
          falling = 1;

        for (i = 0; i < 15 && falling; i++)
          if (platformActive [i] 
            && platformPosition [i].x <= playerPosition.x + 64
            && platformPosition [i].x + 64 >= playerPosition.x + 10
            && platformPosition [i].y <= playerPosition.y + 80
            && platformPosition [i].y + 16 >= playerPosition.y)
            {
              playerPosition.y = platformPosition [i].y + 16;
              falling = 0;
              keyPressedUpDown = false;
            }

This code checks whether the player is standing on a platform. If not, then the character will fall down. This is peformed by the next piece of code. It also decrements the lives counter if the head of your character reaches the bottom of the screen.

        if (falling)
        {
          if (playerPosition.y < -128)
          {
            int leftmost = 0;
            lives--;
            UpdateLives (false);
            BASS_ChannelPlay (soundeffect [4], TRUE);
            for (i = 0; i < 15 && falling; i++)
            {
              if (platformPosition [i].x > 0
                && (platformPosition [i].x < platformPosition [leftmost].x
                  || platformPosition [leftmost].x < 0))
                leftmost = i;
              if (platformActive [i] 
                && platformPosition [i].x <= playerPosition.x + 64
                && platformPosition [i].x + 64 >= playerPosition.x + 10)
                {
                  playerPosition.y = platformPosition [i].y + 16;
                  falling = 0;
                }
            }
            if (falling)
            {
              playerPosition.x = platformPosition [leftmost].x;
              playerPosition.y = platformPosition [leftmost].y + 16;
            }
          }
          playerPosition.y -= falling;
          falling++;
        }
      }
    }

The next piece of code checks if you have pressed cursor up, which triggers a jump when possible.

    if (g_keys->keyDown [VK_UP])
    {
      if (!keyPressedUpDown)
      {
        keyPressedUpDown = true;
        jumping = 20;
        falling = 0;
        BASS_ChannelPlay (soundeffect [3], TRUE);
      }
    }
    else if (keyPressedUpDown && !falling)
      jumping = 1;

What comes after that is the code for moving to the left. Nothing special since it is not possible to scroll backwards in this simple platform game.

    scrolling = false;
    if (g_keys->keyDown [VK_LEFT])
    {
      if (!keyPressedLeftRight || keyPressedLeftRightSince + 10 < tickCount)
      {
        keyPressedLeftRight = true;
        keyPressedLeftRightSince = tickCount;
        if (playerPosition.x > 2)
          playerPosition.x -= 3;
        if (movingswitchedSince + 60 < tickCount)
        {
          movingswitchedSince = tickCount;
          dinosaurMoving = !dinosaurMoving;
        }
        movingDirection = false;
      }
    }

The next code is for moving to the right. When the player hits the center of the x axis, the screen scrolls to the right. This is performed by all enemies and platform moving to the left, while the position of the player remains unchanged.

    else if (g_keys->keyDown [VK_RIGHT])
    {
      if (!keyPressedLeftRight || keyPressedLeftRightSince + 10 < tickCount)
      {
        keyPressedLeftRight = true;
        keyPressedLeftRightSince = tickCount;
        if (playerPosition.x < 320)
          playerPosition.x += 3;
        else
        {
          scrolling = true;
          j = 15;
          for (i = 0; i < 15; i++)
          {
            if (platformActive [i])
            {
              platformPosition [i].x -= 3;
              if (platformPosition [i].x < -64)
              {
                platformActive [i] = false;
                completed++;
              }
            }
            else
              j = i;
          }

The game calculates the positions of some of the platforms in advance. When the game scrolls, it also checks whether it is time to add a new platform to the stack. If so, it also decides whether to insert a gap.

          if (j < 15)
          {
            if (!gap && !(rand () % 6))
              gap = true;
            else
            {
              platformPosition [j].x = platformPosition [rightmost].x + 64;
              if (gap)
                platformPosition [j].x += 100;
              platformPosition [j].y = platformPosition [rightmost].y;
              upward++;
              if (!(rand () % 4))
              {
                int temp = rand () % 120 + 100;
                if (rand () % 2)
                  temp = -temp;
                while (platformPosition [j].y + temp < 0 || platformPosition [j].y + temp > 360)
                {
                  if (platformPosition [j].y - temp < 0 || platformPosition [j].y - temp > 360)
                  {
                    temp /= 4;
                    temp *= 3;
                  }
                  else
                    temp = -temp;
                }
                if (temp < 0)
                  upward = 0;
                platformPosition [j].y += temp;
              }
              platformActive [j] = true;
              rightmost = j;
              gap = false;
            }
          }
        }

Finally, the image of the dinosaur is altered every 60 ticks while he is moving, so that it looks like a simple animation.

        if (movingswitchedSince + 60 < tickCount)
        {
          movingswitchedSince = tickCount;
          dinosaurMoving = !dinosaurMoving;
        }
        movingDirection = true;
      }
    }
    else if (!g_keys->keyDown [VK_LEFT] && !g_keys->keyDown [VK_RIGHT])
      keyPressedLeftRight = false;

The following code implements shooting; it is similar to the analogous code section in the fish part.

    if (g_keys->keyDown [VK_SPACE])
    {
      if (spacePressedSince + 800 < tickCount)
      {
        spacePressed = true;
        spacePressedSince = tickCount;
        for (i = 0; i < 10 && bulletActive [i]; i++);
        if (i < 10)
        {
          if (movingDirection)
            bulletPosition [i].x = playerPosition.x + 64;
          else
            bulletPosition [i].x = playerPosition.x - 32;
          bulletPosition [i].y = playerPosition.y + 60;
          bulletDirection [i] = movingDirection;
          bulletActive [i] = true;
          BASS_ChannelPlay (soundeffect [5], TRUE);
        }
      }
    }
    else
      spacePressed = false;

    if (lastBulletMove + 5 < tickCount)
    {
      lastBulletMove = tickCount;
      for (i = 0; i < 10; i++)
        if (bulletActive [i])
        {
          if (bulletDirection [i])
            bulletPosition [i].x += 5;
          else
            bulletPosition [i].x -= 5;
          bulletPosition [i].y--;
          if (bulletPosition [i].x >= 640 || bulletPosition [i].x < 0 || bulletPosition [i].y < 0)
            bulletActive [i] = false;
        }

Next comes the collision handling.

      for (i = 0; i < 20; i++)
        if (enemyActive [i])
        {
          if (playerPosition.x + 64 >= enemy [i].position.x
            && playerPosition.x <= enemy [i].position.x + enemy [i].width
            && playerPosition.y + 80 >= enemy [i].position.y
            && playerPosition.y <= enemy [i].position.y + enemy [i].height)
          {
            lives--;
            UpdateLives (false);
            enemyActive [i] = false;
            BASS_ChannelPlay (soundeffect [4], TRUE);
          }
          for (j = 0; j < 10 && enemyActive [i]; j++)
              if (bulletActive [j]
                && bulletPosition [j].x + 32 >= enemy [i].position.x
                && bulletPosition [j].x <= enemy [i].position.x + enemy [i].width
                && bulletPosition [j].y + 32 >= enemy [i].position.y
                && bulletPosition [j].y <= enemy [i].position.y + enemy [i].height)
              {
                BASS_ChannelPlay (soundeffect [2], TRUE);
                bulletActive [j] = false;
                enemyActive [i] = false;
              }

Then, the behaviour of the enemies. In the dinosaur part only two types of enemies appear: worms and lions. The worms shoot straight, while the lions shoot in various directions, similar to the crabs in the fish part.

          if (enemyActive [i])
          {
            if (scrolling)
              enemy [i].position.x -= 3;
            if (enemy [i].position.x < -enemy [i].width)
              enemyActive [i] = false;
            else
            {
              switch (enemy [i].type)
              {
                case Worm:
                if (enemy [i].variable [1] + 1600 < tickCount)
                {
                  enemy [i].variable [1] = tickCount;
                  if (rand () % 3)
                  {
                    for (j = 0; j < 20 && enemyActive [j]; j++);
                    if (j < 20)
                    {
                      BASS_ChannelPlay (soundeffect [0], TRUE);
                      enemy [j].type = EnemyBullet;
                      enemy [j].width = 32;
                      enemy [j].height = 32;
                      enemy [j].position.x = enemy [i].position.x - 32;
                      enemy [j].position.y = enemy [i].position.y + 80;
                      enemy [j].whereToGo.y = 0;
                      enemyActive [j] = true;
                    }
                  }
                }
                break;

                case Lion:
                if (enemy [i].variable [1] + 800 < tickCount)
                {
                  enemy [i].variable [1] = tickCount;
                  if (rand () % 2)
                  {
                    for (j = 0; j < 20 && enemyActive [j]; j++);
                    if (j < 20)
                    {
                      BASS_ChannelPlay (soundeffect [0], TRUE);
                      enemy [j].type = EnemyBullet;
                      enemy [j].width = 32;
                      enemy [j].height = 32;
                      enemy [j].position.x = enemy [i].position.x - 32;
                      enemy [j].position.y = enemy [i].position.y + 80;
                      enemy [j].whereToGo.y = 1 + rand () % 3;
                      if (rand () % 2)
                        enemy [j].whereToGo.y = -enemy [j].whereToGo.y;
                      enemyActive [j] = true;
                    }
                  }
                }
                break;

              case EnemyBullet:
                enemy [i].position.x -= 5;
                enemy [i].position.y += enemy [i].whereToGo.y;
                for (j = 0; j < 20 && enemyActive [i]; j++)
                  if (enemyActive [j] && j != i
                    && enemy [i].position.x + enemy [i].width >= enemy [j].position.x
                    && enemy [i].position.x <= enemy [j].position.x + enemy [j].width
                    && enemy [i].position.y + enemy [i].height >= enemy [j].position.y
                    && enemy [i].position.y <= enemy [j].position.y + enemy [j].height)
                    enemyActive [i] = false;
                break;
              }
            }
          }
        }

When a bullet hits a platform, it disappears. This is implemented by the following code.

        for (i = 0; i < 15; i++)
          if (platformActive [i])
            for (j = 0; j < 10; j++)
              if (bulletActive [j]
                && bulletPosition [j].x + 32 >= platformPosition [i].x
                && bulletPosition [j].x <= platformPosition [i].x + 64
                && bulletPosition [j].y + 32 >= platformPosition [i].y
                && bulletPosition [j].y <= platformPosition [i].y + 16)
                bulletActive [j] = false;
    }

Next, the initialization code for the enemies, similar to the code in the fish part.

    if (upward > 2
      && lastEnemyAppeared + 2 < completed
      && ((lastEnemyAppeared + 20 < completed)
      || (lastEnemyAppeared + 5 - completed / 100 < completed && rand () % 2)))
    {
      for (i = 0; i < 20 && enemyActive [i]; i++);
      if (i < 20)
      {
        for (j = 0; j < 10; j++)
          enemy [i].variable [j] = 0;
        switch (rand () % 10)
        {
        case 0:
        case 1:
        case 2:
          enemy [i].type = Lion;
          enemy [i].width = 128;
          enemy [i].height = 118;
          enemy [i].position.x = platformPosition [rightmost].x;
          enemy [i].position.y = platformPosition [rightmost].y + 16;
          break;

        default:
          enemy [i].type = Worm;
          enemy [i].width = 64;
          enemy [i].height = 98;
          enemy [i].position.x = platformPosition [rightmost].x;
          enemy [i].position.y = platformPosition [rightmost].y + 16;
          break;
        }
        enemy [i].moving = false;
        enemyActive [i] = true;
        lastEnemyAppeared = completed;
      }
    }
  }
}

Finally, the drawing method, containing the OpenGL commands for displaying the sprites and other images.

void Game::Draw (float tickCount)
{
  int i;

  glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  switch (gameState)
  {
  case TitlePicture:
    glEnable (GL_TEXTURE_2D);
    glBindTexture (GL_TEXTURE_2D, texture_titlescreen);
    glColor3f (1.0f, 1.0f, 1.0f);
    glBegin (GL_QUADS);
      glTexCoord2f (0.0f, 0.0f); glVertex2f ((640 - 512) / 2 + 0, 0);
      glTexCoord2f (1.0f, 0.0f); glVertex2f ((640 - 512) / 2 + 512, 0);
      glTexCoord2f (1.0f, 1.0f); glVertex2f ((640 - 512) / 2 + 512, 512);
      glTexCoord2f (0.0f, 1.0f); glVertex2f ((640 - 512) / 2 + 0, 512);
    glEnd ();
    glDisable (GL_TEXTURE_2D);
    break;

  case GameOver:
    glEnable (GL_TEXTURE_2D);
    glBindTexture (GL_TEXTURE_2D, texture_gameover);
    glColor3f (1.0f, 1.0f, 1.0f);
    glBegin (GL_QUADS);
      glTexCoord2f (0.0f, 0.0f); glVertex2f ((640 - 512) / 2 + 0, 0);
      glTexCoord2f (1.0f, 0.0f); glVertex2f ((640 - 512) / 2 + 512, 0);
      glTexCoord2f (1.0f, 1.0f); glVertex2f ((640 - 512) / 2 + 512, 512);
      glTexCoord2f (0.0f, 1.0f); glVertex2f ((640 - 512) / 2 + 0, 512);
    glEnd ();
    glDisable (GL_TEXTURE_2D);
    break;

  case GameCompleted:
    glEnable (GL_TEXTURE_2D);
    glBindTexture (GL_TEXTURE_2D, texture_gamecompleted);
    glColor3f (1.0f, 1.0f, 1.0f);
    glBegin (GL_QUADS);
      glTexCoord2f (0.0f, 0.0f); glVertex2f ((640 - 512) / 2 + 0, 0);
      glTexCoord2f (1.0f, 0.0f); glVertex2f ((640 - 512) / 2 + 512, 0);
      glTexCoord2f (1.0f, 1.0f); glVertex2f ((640 - 512) / 2 + 512, 512);
      glTexCoord2f (0.0f, 1.0f); glVertex2f ((640 - 512) / 2 + 0, 512);
    glEnd ();
    glDisable (GL_TEXTURE_2D);
    break;

  case ActualGame:
    glEnable (GL_TEXTURE_2D);
    glBindTexture (GL_TEXTURE_2D, texture_lives);
    glColor3f (1.0f, 1.0f, 1.0f);
    glBegin (GL_QUADS);
      glTexCoord2f (0.0f, 0.0f); glVertex2f (0, 480 - 16);
      glTexCoord2f (1.0f, 0.0f); glVertex2f (64, 480 - 16);
      glTexCoord2f (1.0f, 1.0f); glVertex2f (64, 480);
      glTexCoord2f (0.0f, 1.0f); glVertex2f (0, 480);
    glEnd ();
    glDisable (GL_TEXTURE_2D);

    glEnable (GL_TEXTURE_2D);
    glBindTexture (GL_TEXTURE_2D, texture [0]);
    glColor3f (1.0f, 1.0f, 1.0f);
    glBegin (GL_QUADS);
      glTexCoord2f (0.0f, 0.0f); glVertex2f (playerPosition.x, playerPosition.y);
      glTexCoord2f (1.0f, 0.0f); glVertex2f (playerPosition.x + 64, playerPosition.y);
      glTexCoord2f (1.0f, 1.0f); glVertex2f (playerPosition.x + 64, playerPosition.y + 32);
      glTexCoord2f (0.0f, 1.0f); glVertex2f (playerPosition.x, playerPosition.y + 32);
    glEnd ();
    glDisable (GL_TEXTURE_2D);

    for (i = 0; i < 10; i++)
      if (bulletActive [i])
      {
        glEnable (GL_TEXTURE_2D);
        glBindTexture (GL_TEXTURE_2D, texture [1]);
        glColor3f (1.0f, 1.0f, 1.0f);
        glBegin (GL_QUADS);
          glTexCoord2f (0.0f, 0.0f); glVertex2f (bulletPosition [i].x, bulletPosition [i].y);
          glTexCoord2f (1.0f, 0.0f); glVertex2f (bulletPosition [i].x + 64, bulletPosition [i].y);
          glTexCoord2f (1.0f, 1.0f); glVertex2f (bulletPosition [i].x + 64, bulletPosition [i].y + 32);
          glTexCoord2f (0.0f, 1.0f); glVertex2f (bulletPosition [i].x, bulletPosition [i].y + 32);
        glEnd ();
        glDisable (GL_TEXTURE_2D);
      }

    for (i = 0; i < 20; i++)
      if (enemyActive [i])
      {
        glEnable (GL_TEXTURE_2D);
        switch (enemy [i].type)
        {
        case Kraken:
          glBindTexture (GL_TEXTURE_2D, texture [6]);
          break;

        case Cancer:
          glBindTexture (GL_TEXTURE_2D, texture [3]);
          break;

        case Seastar:
          glBindTexture (GL_TEXTURE_2D, texture [7]);
          break;

        case Jellyfish:
          glBindTexture (GL_TEXTURE_2D, texture [5]);
          break;

        case EnemyBullet:
          glBindTexture (GL_TEXTURE_2D, texture [2]);
          break;

        case CancerBullet:
          glBindTexture (GL_TEXTURE_2D, texture [4]);
          break;
        }
        glColor3f (1.0f, 1.0f, 1.0f);
        switch (enemy [i].type)
        {
        case Kraken:
          glBegin (GL_QUADS);
            glTexCoord2f (0.0f, 0.0f); glVertex2f (enemy [i].position.x, enemy [i].position.y);
            glTexCoord2f (1.0f, 0.0f); glVertex2f (enemy [i].position.x + 128, enemy [i].position.y);
            glTexCoord2f (1.0f, 1.0f); glVertex2f (enemy [i].position.x + 128, enemy [i].position.y 
            + 128);
            glTexCoord2f (0.0f, 1.0f); glVertex2f (enemy [i].position.x, enemy [i].position.y + 128);
          glEnd ();
          break;

        case Jellyfish:
          glBegin (GL_QUADS);
            glTexCoord2f (0.0f, 0.0f); glVertex2f (enemy [i].position.x, enemy [i].position.y);
            glTexCoord2f (1.0f, 0.0f); glVertex2f (enemy [i].position.x + 64, enemy [i].position.y);
            glTexCoord2f (1.0f, 1.0f); glVertex2f (enemy [i].position.x + 64, enemy [i].position.y 
            + 128);
            glTexCoord2f (0.0f, 1.0f); glVertex2f (enemy [i].position.x, enemy [i].position.y + 128);
          glEnd ();
          break;

        case Seastar:
          glBegin (GL_QUADS);
            glTexCoord2f (0.0f, 0.0f); glVertex2f (enemy [i].position.x, enemy [i].position.y);
            glTexCoord2f (1.0f, 0.0f); glVertex2f (enemy [i].position.x + 64, enemy [i].position.y);
            glTexCoord2f (1.0f, 1.0f); glVertex2f (enemy [i].position.x + 64, enemy [i].position.y + 64);
            glTexCoord2f (0.0f, 1.0f); glVertex2f (enemy [i].position.x, enemy [i].position.y + 64);
          glEnd ();
          break;

        case Cancer:
          glBegin (GL_QUADS);
            glTexCoord2f (0.0f, 0.0f); glVertex2f (enemy [i].position.x, enemy [i].position.y);
            glTexCoord2f (1.0f, 0.0f); glVertex2f (enemy [i].position.x + 128, enemy [i].position.y);
            glTexCoord2f (1.0f, 1.0f); glVertex2f (enemy [i].position.x + 128, enemy [i].position.y 
            + 64);
            glTexCoord2f (0.0f, 1.0f); glVertex2f (enemy [i].position.x, enemy [i].position.y + 64);
          glEnd ();
          break;

        case EnemyBullet:
          glBegin (GL_QUADS);
            glTexCoord2f (0.0f, 0.0f); glVertex2f (enemy [i].position.x, enemy [i].position.y);
            glTexCoord2f (1.0f, 0.0f); glVertex2f (enemy [i].position.x + 64, enemy [i].position.y);
            glTexCoord2f (1.0f, 1.0f); glVertex2f (enemy [i].position.x + 64, enemy [i].position.y + 32);
            glTexCoord2f (0.0f, 1.0f); glVertex2f (enemy [i].position.x, enemy [i].position.y + 32);
          glEnd ();
          break;

        case CancerBullet:
          glBegin (GL_QUADS);
            glTexCoord2f (0.0f, 0.0f); glVertex2f (enemy [i].position.x, enemy [i].position.y);
            glTexCoord2f (1.0f, 0.0f); glVertex2f (enemy [i].position.x + 32, enemy [i].position.y);
            glTexCoord2f (1.0f, 1.0f); glVertex2f (enemy [i].position.x + 32, enemy [i].position.y + 32);
            glTexCoord2f (0.0f, 1.0f); glVertex2f (enemy [i].position.x, enemy [i].position.y + 32);
          glEnd ();
          break;
        }
        glDisable (GL_TEXTURE_2D);
      }
      break;

    case ActualGame_Dinosaur:
      glEnable (GL_TEXTURE_2D);
      glBindTexture (GL_TEXTURE_2D, texture_lives);
      glColor3f (1.0f, 1.0f, 1.0f);
      glBegin (GL_QUADS);
        glTexCoord2f (0.0f, 0.0f); glVertex2f (0, 480 - 16);
        glTexCoord2f (1.0f, 0.0f); glVertex2f (64, 480 - 16);
        glTexCoord2f (1.0f, 1.0f); glVertex2f (64, 480);
        glTexCoord2f (0.0f, 1.0f); glVertex2f (0, 480);
      glEnd ();
      glDisable (GL_TEXTURE_2D);

      glEnable (GL_TEXTURE_2D);
      if (dinosaurMoving)
        glBindTexture (GL_TEXTURE_2D, texture [10]);
      else
        glBindTexture (GL_TEXTURE_2D, texture [9]);
      glColor3f (1.0f, 1.0f, 1.0f);
      glBegin (GL_QUADS);
        if (movingDirection)
        {
          glTexCoord2f (0.0f, 0.0f); glVertex2f (playerPosition.x, playerPosition.y);
          glTexCoord2f (1.0f, 0.0f); glVertex2f (playerPosition.x + 64, playerPosition.y);
          glTexCoord2f (1.0f, 1.0f); glVertex2f (playerPosition.x + 64, playerPosition.y + 128);
          glTexCoord2f (0.0f, 1.0f); glVertex2f (playerPosition.x, playerPosition.y + 128);
        }
        else
        {
          glTexCoord2f (0.0f, 0.0f); glVertex2f (playerPosition.x + 64, playerPosition.y);
          glTexCoord2f (1.0f, 0.0f); glVertex2f (playerPosition.x, playerPosition.y);
          glTexCoord2f (1.0f, 1.0f); glVertex2f (playerPosition.x, playerPosition.y + 128);
          glTexCoord2f (0.0f, 1.0f); glVertex2f (playerPosition.x + 64, playerPosition.y + 128);
        }
      glEnd ();
      glDisable (GL_TEXTURE_2D);

      for (i = 0; i < 15; i++)
        if (platformActive [i])
        {
          glEnable (GL_TEXTURE_2D);
          glBindTexture (GL_TEXTURE_2D, texture [8]);
          glColor3f (1.0f, 1.0f, 1.0f);
          glBegin (GL_QUADS);
            glTexCoord2f (0.0f, 0.0f); glVertex2f (platformPosition [i].x, platformPosition [i].y);
            glTexCoord2f (1.0f, 0.0f); glVertex2f (platformPosition [i].x + 64, platformPosition [i].y);
            glTexCoord2f (1.0f, 1.0f); glVertex2f (platformPosition [i].x + 64, platformPosition [i].y 
            + 16);
            glTexCoord2f (0.0f, 1.0f); glVertex2f (platformPosition [i].x, platformPosition [i].y + 16);
          glEnd ();
          glDisable (GL_TEXTURE_2D);
        }

      for (i = 0; i < 10; i++)
        if (bulletActive [i])
        {
          glEnable (GL_TEXTURE_2D);
          glBindTexture (GL_TEXTURE_2D, texture [11]);
          glColor3f (1.0f, 1.0f, 1.0f);
          glBegin (GL_QUADS);
            glTexCoord2f (0.0f, 0.0f); glVertex2f (bulletPosition [i].x, bulletPosition [i].y);
            glTexCoord2f (1.0f, 0.0f); glVertex2f (bulletPosition [i].x + 32, bulletPosition [i].y);
            glTexCoord2f (1.0f, 1.0f); glVertex2f (bulletPosition [i].x + 32, bulletPosition [i].y + 32);
            glTexCoord2f (0.0f, 1.0f); glVertex2f (bulletPosition [i].x, bulletPosition [i].y + 32);
          glEnd ();
          glDisable (GL_TEXTURE_2D);
        }

      for (i = 0; i < 20; i++)
        if (enemyActive [i])
        {
          glEnable (GL_TEXTURE_2D);
          switch (enemy [i].type)
          {
          case Lion:
            glBindTexture (GL_TEXTURE_2D, texture [13]);
            break;

          case Worm:
            glBindTexture (GL_TEXTURE_2D, texture [14]);
            break;

          case EnemyBullet:
            glBindTexture (GL_TEXTURE_2D, texture [12]);
            break;
          }
          glColor3f (1.0f, 1.0f, 1.0f);
          switch (enemy [i].type)
          {
          case Lion:
            glBegin (GL_QUADS);
              glTexCoord2f (0.0f, 0.0f); glVertex2f (enemy [i].position.x, enemy [i].position.y);
              glTexCoord2f (1.0f, 0.0f); glVertex2f (enemy [i].position.x + 128, enemy [i].position.y);
              glTexCoord2f (1.0f, 1.0f); glVertex2f (enemy [i].position.x + 128, enemy [i].position.y 
              + 128);
              glTexCoord2f (0.0f, 1.0f); glVertex2f (enemy [i].position.x, enemy [i].position.y + 128);
            glEnd ();
            break;

          case Worm:
            glBegin (GL_QUADS);
              glTexCoord2f (0.0f, 0.0f); glVertex2f (enemy [i].position.x, enemy [i].position.y);
              glTexCoord2f (1.0f, 0.0f); glVertex2f (enemy [i].position.x + 64, enemy [i].position.y);
              glTexCoord2f (1.0f, 1.0f); glVertex2f (enemy [i].position.x + 64, enemy [i].position.y 
              + 128);
              glTexCoord2f (0.0f, 1.0f); glVertex2f (enemy [i].position.x, enemy [i].position.y + 128);
            glEnd ();
            break;

          case EnemyBullet:
            glBegin (GL_QUADS);
              glTexCoord2f (0.0f, 0.0f); glVertex2f (enemy [i].position.x, enemy [i].position.y);
              glTexCoord2f (1.0f, 0.0f); glVertex2f (enemy [i].position.x + 32, enemy [i].position.y);
              glTexCoord2f (1.0f, 1.0f); glVertex2f (enemy [i].position.x + 32, enemy [i].position.y 
              + 32);
              glTexCoord2f (0.0f, 1.0f); glVertex2f (enemy [i].position.x, enemy [i].position.y + 32);
            glEnd ();
            break;
          }
        }
      break;
  }
}

And ... the routine to update the part of the screen in which the number of lives is displayed.

void Game::UpdateLives (bool generate = false)
{
  char text [9], temp [4];
  int color [3] = {0, 0, 64};
  TextObject textObject;
  if (lives < 1)
  {
    glClearColor (0, 0, 0.3f, 1);
    gameState = GameOver;
    BASS_ChannelPlay (soundeffect [1], TRUE);
    return;
  }
  strcpy (text, "Lives: ");
  strcat (text, itoa (lives, temp, 10));
  textObject.SetText (text);
  bitmap_lives.Clear (color);
  bitmap_lives.SetRenderTextFont ("Arial", 16);
  bitmap_lives.SetRenderTextColor (0xffff00);
  bitmap_lives.RenderText (&textObject, Align_Left);
  if (generate)
    bitmap_lives.GenerateTexture (texture_lives);
  else
    bitmap_lives.UpdateTexture (texture_lives);
}

The Graphics

To get the game to work, you also need some images. Here they are. You can replace them with your own ones provided they have the same size. Maybe you are more artistically talented than I am.


bullet.bmp


dino_bullet.bmp


enemy_bullet.bmp


enemy_bulletdino.bmp


enemy_cancerbullet.bmp


platform.bmp


dino_move.bmp


dino_stand.bmp


fish.bmp


enemy_cancer.bmp


enemy_jellyfish.bmp


enemy_kraken.bmp


enemy_lion.bmp


enemy_seastar.bmp


enemy_worm.bmp


titlescreen.bmp


gameover.bmp


gamecompleted.bmp

The other files (audio) are available at my homepage.

Adok